/* John Zigler and John Spicher
 * Cryptography, Fall 2011
 * Final Project
 */

package cryptofinal;
import java.io.*;
import java.util.*;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
public class Main {
    //Declare variables
    private static String encryptedMessage,
            encryptedMessagePath,
            decryptedMessage,
            dictionaryPath;
    private static CharacterNode dictionary;
    private static final String ALPHABET = "abcdefghijklmnopqrstuvwxyz";
    private static final char[] mostCommon = "ETAONRISHDLFCMUGYPWBVKXJQZ".toCharArray();

    /* Main method
     * Initializes dictionary path. This has bee hard-coded for testing purposes
     * and could potentially be made dynamic, but that seems outside of the
     * project scope. Loads the dictionary, prompts the user to choose a file
     * for decryption, grabs the data from the file, and begins the decryption
     * sequence.
     */
    public static void main(String[] args) {
        dictionaryPath = //args[0]; //This is hard-coded for testing purposes
                "C:"+File.separator+
                "Users"+File.separator+
                "John"+File.separator+
                "Documents"+File.separator+
                "Homework"+File.separator+
                "Cryptography"+File.separator+
                "decrypter"+File.separator+
                "trunk"+File.separator+
                "dictionary.txt";
        loadDictionary();
        //Get the path of the message to be decrypted
        JFileChooser messageChooser = new JFileChooser();
            messageChooser.setDialogTitle("Choose Message");
            messageChooser.setFileFilter(
                    new FileNameExtensionFilter("Text files", "txt"));
            int returnVal = messageChooser.showOpenDialog(null);
            if(returnVal == JFileChooser.APPROVE_OPTION)
                encryptedMessagePath = messageChooser.getSelectedFile().getAbsolutePath();
            else{
                System.out.println("Something failed while opening message");
                System.exit(1);
            }
        //Get the message from the file
        String nextLine = null;
        encryptedMessage = "";
        try{FileReader FR = new FileReader(encryptedMessagePath);
            BufferedReader reader = new BufferedReader(FR);
            nextLine = reader.readLine();
            while (nextLine!=null){
                encryptedMessage+=nextLine+"\n";
                nextLine = reader.readLine();
            }
            reader.close();}
        catch (IOException e){System.out.println("Something didn't work while opening "+encryptedMessagePath);}
        System.out.println(encryptedMessage);
        //Initialize decrypted message
        char[] temp = encryptedMessage.toCharArray();
        for (int i=0;i<encryptedMessage.length();i++)
            if (Character.isLetter(encryptedMessage.charAt(i)))
                temp[i]='_';
        decryptedMessage = String.valueOf(temp);
        //Begin decryption sequence
        guesser(mostFrequentChars(encryptedMessage), mostCommon);
    }

    /* loadDictionary method
     * Opens the file at dictionaryPath, which is expected to be a text file
     * with one word on each line. Adds each line in the file to the
     * CharacterNode dictionary.
     */
    private static void loadDictionary(){
        System.out.println("Loading dictionary...");
        dictionary = new CharacterNode(' ');
        try{FileReader FR = new FileReader(dictionaryPath);
            BufferedReader reader = new BufferedReader(FR);
            String nextLine = reader.readLine();
            while (nextLine!=null){
                dictionary.add(nextLine);
                nextLine = reader.readLine();
            }
            reader.close();}
        catch (IOException e){System.out.println(
                "Something didn't work while opening "+dictionaryPath);}
        System.out.println("Dictionary loaded");
    }

    /* mostFrequentChars method
     * Accepts as parameters the text to analyze. Counts the occurances of each
     * letter within the text, and returns an array of the characters sorted by
     * frequency in descending order.
     */
    private static char[] mostFrequentChars(String text){
        int[] frequency = getFrequency(text);
        char[] inOrder = new char[ALPHABET.length()];
        int maxIndex=-1, maxCount;
        for (int i=0;i<inOrder.length;i++){
            maxCount=-1;
            for (int j=0;j<frequency.length;j++){
                if (frequency[j]>maxCount){
                    maxIndex=j;
                    maxCount=frequency[j];
                }
            }
            inOrder[i]=ALPHABET.charAt(maxIndex);
            frequency[maxIndex]=-1;
        }
        return inOrder;
    }

    /* getFrequency method
     * Accepts as parameters the text to analyze. Counts the occurances of each
     * letter within the text. Returns an array of integers, the length of the
     * alphabet, with the integer value in each index representing the number of
     * occurances of the letter at that index in the alphabet.
     */
    private static int[] getFrequency(String text){
        int[] frequency = new int[ALPHABET.length()];
        while (!text.equals("")){
            int x=Character.getNumericValue(text.charAt(0))-10;
            if (x>=0 && x<=25) frequency[x]++;
            text=text.substring(1);
        }
        return frequency;
    }

    /* guesser method
     * The workhorse of the solution algorithm. Accepts as parameters an ordered
     * array of the most frequent characters in the ciphertext that have not yet
     * been substituted, and an ordered array of the most common letters in the
     * English language that have not yet been used. These arrays should have
     * the same size. If they are empty, then the entire alphabet has been
     * solved without errors, and the method returns true. If the arrays are not
     * empty, substitutes most frequent character with most common letter, and
     * checks for errors. If errors exist, tries to substitute the most frequent
     * with the next most common. If no errors exist, makes new arrays that are
     * subsets of the frequencyList and mostCommonList, excluding the letters
     * that have just been solved, and recursively calls itself to solve the
     * rest of the alpahbet. If this recursive call returns true, then a
     * solution has been found, and returns true. Otherwise, continues
     * iterating. If no letters can be found to substitute for the most frequent
     * character, returns false.
     */
    private static boolean guesser(char[] frequencyList, char[] mostCommonList){
        if (frequencyList.length == 0) {
    		System.out.println(decryptedMessage);
    		return true;
    	}
        int index=0;
        char[] subFreqList = new char[frequencyList.length-1],
                subCommonList = new char[mostCommonList.length-1];
        System.arraycopy(
                    frequencyList, 1, subFreqList, 0, subFreqList.length);
        System.arraycopy(
                    mostCommonList, 1, subCommonList, 0, subCommonList.length);
        char encryptedChar = frequencyList[0],
                charToReplace;
        while (index<mostCommonList.length){
            charToReplace = mostCommonList[index];
            swap(encryptedChar, charToReplace);

            //check for errors
            if (checker(charToReplace)) { //no errors found
                System.arraycopy(mostCommonList, 0, subCommonList, 0, index);
                if (guesser(subFreqList, subCommonList)) return true;
            }
            index++;
        }//End while
        swap(encryptedChar, '_');
        return false;
    }//End method

    /* swap method
     * Accepts as parameters the character that is being decrypted, and the
     * letter that is the most likely decryption of that character. Looks
     * through the encrypted message character by character. If the specified
     * character is found, notes the index, and replaces the character at the
     * same index in the decrypted message with the specified letter in the
     * correct case.
     */
    private static void swap(char toSwap, char decryptionGuess){
        char[] temp = decryptedMessage.toCharArray();
        for (int i=0;i<encryptedMessage.length();i++){
            if (Character.toLowerCase(encryptedMessage.charAt(i))==Character.toLowerCase(toSwap)){
                if (Character.isLowerCase(encryptedMessage.charAt(i)))
                    temp[i]=Character.toLowerCase(decryptionGuess);
                else
                    temp[i]=Character.toUpperCase(decryptionGuess);
            }
        }
        decryptedMessage = String.valueOf(temp);
    }

    /* checker method
     * Accepts as parameter the character which has just been added to the
     * decrypted message. Looks through all of the words in the decrypted
     * message, and collects all of the completed words that contain the
     * specified character. These words must have just been completed, and
     * therefore are the newest completed words. If any of these words are not
     * in the dictionary, returns false. Returns true if no errors are found.
     */
    private static boolean checker(char c){
        //get list of words to check
        Stack<String> finishedWords = new Stack();
        StringTokenizer text = new StringTokenizer(decryptedMessage," .,;:\"/&!?\n");
        String nextWord;
        while(text.hasMoreTokens()){
            nextWord=text.nextToken();
            if (nextWord.contains(c+"")&&!nextWord.contains("_"))
                finishedWords.add(nextWord);
        }
        while(!finishedWords.empty()){
            nextWord=finishedWords.pop();
            if (!dictionary.contains(nextWord)){
                //System.out.println("Dictionary does not include '"+nextWord+"'.");
                return false;
            }
        }
        return true;
    }
}
